/*
  联合类型在类型编程中是比较特殊的，TypeScript 对它做了专门的处理，写法上可以简化，但也增加了一些认知成本。
*/

{
  /*
    分布式条件类型

    当类型参数为联合类型，并且在条件类型左边直接引用该类型参数的时候，TypeScript 会把每一个元素单独传入来做类型运算，最后再合并成联合类型，这种语法叫做分布式条件类型。
  */

  type Union = 'a' | 'b' | 'c';

  // 我们想把其中的 a 大写，就可以这样写：

  type UppercaseA<Item extends string> = Item extends 'a' ? Uppercase<Item> : Item;

  type res = UppercaseA<Union>; // type res = "b" | "c" | "A"

  /*
    可以看到，我们类型参数 Item 约束为 string，条件类型的判断中也是判断是否是 a，但传入的是联合类型

    这就是 TypeScript 对联合类型在条件类型中使用时的特殊处理：会把联合类型的每一个元素单独传入做类型计算，最后合并。

    这和联合类型遇到字符串时的处理一样：
  */

  type str = `${Union}~~`; // type str = "a~~" | "b~~" | "c~~"

  /*
    这样确实是简化了类型编程逻辑的，不需要递归提取每个元素再处理。

    TypeScript 之所以这样处理联合类型也很容易理解，因为联合类型的每个元素都是互不相关的，不像数组、索引、字符串那样元素之间是有关系的。所以设计成了每一个单独处理，最后合并。
  */
}

{
  type Camelcase<Str extends string> = Str extends `${infer Left}_${infer Right}${infer Rest}`
    ? `${Left}${Uppercase<Right>}${Camelcase<Rest>}`
    : Str;

  type res = Camelcase<'aa_bb_ccc'>;
}

{
  // 对字符串数组做 Camelcase，那就要递归处理每一个元素

  type Camelcase<Str extends string> = Str extends `${infer Left}_${infer Right}${infer Rest}`
    ? `${Left}${Uppercase<Right>}${Camelcase<Rest>}`
    : Str;

  type CamelcaseArr<Arr extends unknown[], Result extends unknown[] = []> = Arr extends [
    infer Item,
    ...infer RestArr
  ]
    ? [...Result, Camelcase<Item & string>, ...CamelcaseArr<RestArr>]
    : Result;

  type res = CamelcaseArr<['aa_bb_ccc', 'dd_aa_ff']>;

  /*
    类型参数 Arr 为待处理数组，类型参数 Result 为处理后的数组。
    递归提取每一个元素做 Camelcase，因为 Camelcase 要求传入 string，这里要 & string 来变成 string 类型。
  */
}

{
  /*
    那如果是联合类型呢？
    联合类型不需要递归提取每个元素，TypeScript 内部会把每一个元素传入单独做计算，之后把每个元素的计算结果合并成联合类型。
  */

  type CaselcaseUnion<Item extends string> =
    Item extends `${infer Left}_${infer Right}${infer Rest}`
      ? `${Left}${Uppercase<Right>}${CaselcaseUnion<Rest>}`
      : Item;

  type res = CaselcaseUnion<'aa_bb_ccc' | 'dd_aa_ff'>;
}

/*
  这不和单个字符串的处理没区别么？
  没错，对联合类型的处理和对单个类型的处理没什么区别，TypeScript 会把每个单独的类型拆开传入。不需要像数组类型那样需要递归提取每个元素做处理。

  确实简化了很多，好像都是优点？

  也不全是，其实这样处理也增加了一些认知成本，不信我们再来看个例子：
*/

{
  type IsUnion<A, B = A> = A extends B ? ([B] extends [A] ? false : true) : never;

  // type res = true
  type res = IsUnion<'a' | 'b' | 'c'>;

  // type res2 = false
  type res2 = IsUnion<['a' | 'b' | 'c']>;

  // 先看这样一个类型

  type TestUnion<A, B = A> = A extends A
    ? {
        a: A;
        b: B;
      }
    : never;

  /*
    type TestUnionResult = {
      a: "a";
      b: "a" | "b" | "c";
    } | {
        a: "b";
        b: "a" | "b" | "c";
    } | {
        a: "c";
        b: "a" | "b" | "c";
    }
  */
  type TestUnionResult = TestUnion<'a' | 'b' | 'c'>;

  /*
    类型参数 A、B 是待判断的联合类型，B 默认值为 A，也就是同一个类型。

    A extends A 这段看似没啥意义，主要是为了触发分布式条件类型，让 A 的每个类型单独传入。

    [B] extends [A] 这样不直接写 B 就可以避免触发分布式条件类型，那么 B 就是整个联合类型。

    B 是联合类型整体，而 A 是单个类型，自然不成立，而其它类型没有这种特殊处理，A 和 B 都是同一个，怎么判断都成立。

    利用这个特点就可以判断出是否是联合类型。

    其中有两个点比较困惑，我们重点记一下：

    当 A 是联合类型时：

    A extends A 这种写法是为了触发分布式条件类型，让每个类型单独传入处理的，没别的意义。

    A extends A 和 [A] extends [A] 是不同的处理，前者是单个类型和整个类型做判断，后者两边都是整个联合类型，因为只有 extends 左边直接是类型参数才会触发分布式条件类型。
  */
}

{
  // bem 是 css 命名规范，用 block__element--modifier 的形式来描述某个区块下面的某个元素的某个状态的样式
  // 那么我们可以写这样一个高级类型，传入 block、element、modifier，返回构造出的 class 名：

  // 数组转联合类型
  type union = ['a', 'b'][number]; // type union = 'a' | 'b'

  // 那么 BEM 就可以这样实现：

  type BEM<
    Block extends string,
    Element extends string[],
    Modifier extends string[],
  > = `${Block}__${Element[number]}--${Modifier[number]}`;

  type bemResult = BEM<'guang', ['aaa', 'bbb'], ['warning', 'success']>;
}

{
  // AllCombinations

  /*
    我们再来实现一个全组合的高级类型，也是联合类型相关的：
    希望传入 'A' | 'B' 的时候，能够返回所有的组合： 'A' | 'B' | 'BA' | 'AB'。
  */

  type Combination<A extends string, B extends string> = A | B | `${A}${B}` | `${B}${A}`;

  type res = Combination<'a', 'b'>;

  type AllCombinations<A extends string, B extends string = A> = A extends A
    ? Combination<A, AllCombinations<Exclude<B, A>>>
    : never;

  // type res2 = "a" | "b" | "c" | "bc" | "cb" | "ab" | "ac" | "abc" | "acb" | "ba" | "ca" | "bca" | "cba" | "bac" | "cab"
  type res2 = AllCombinations<'a' | 'b' | 'c'>;
}

/*
  联合类型中的每个类型都是相互独立的，TypeScript 对它做了特殊处理，也就是遇到字符串类型、条件类型的时候会把每个类型单独传入做计算，最后把每个类型的计算结果合并成联合类型。

  条件类型左边是联合类型的时候就会触法这种处理，叫做分布式条件类型。

  有两点特别要注意：

  A extends A 不是没意义，意义是取出联合类型中的单个类型放入 A

  A extends A 才是分布式条件类型， [A] extends [A] 就不是了，只有左边是单独的类型参数才可以。
*/
